package com.flsldl.mydb.backend.dm.logger;

import com.flsldl.mydb.backend.utils.Panic;
import com.flsldl.mydb.backend.utils.Parser;
import com.flsldl.mydb.common.Error;
import com.google.common.primitives.Bytes;

import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.util.Arrays;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @ClassName LoggerImpl
 * @Description TODO
 * @Author @O_o @Date 2023/2/20 上午 10:39
 *
 *日志文件读写
 *
 *日志文件标准格式为：
 *[XChecksum][Log1][Log2]...[LogN][BadTail]
 *XChecksum 为后续所有日志计算的Checksum，int类型
 *
 *每条正确日志的格式为：
 *[Size][Checksum][Data]
 *Size 4字节int 标识Data长度
 *Checksum 4字节int
 */
public class LoggerImpl implements Logger{

    // 这个 SEED 取值防止【哈希冲突】
    private static final int SEED = 13331;

    // 日志 SIZE 数据长度
    private static final int OF_SIZE = 0;
    // 日志 Checksum 的所在位置
    private static final int OF_CHECKSUM = OF_SIZE + 4;
    // 日志 data[] 数据所在位置
    private static final int OF_DATA = OF_CHECKSUM + 4;

    public static final String LOG_SUFFIX = ".log";

    private RandomAccessFile file;
    private FileChannel fc;
    private Lock lock;

    // 当前日志指针的位置
    private long position;
    // 初始化时记录，log 操作不更新
    private long fileSize;
    private int xChecksum;

    LoggerImpl(RandomAccessFile raf, FileChannel fc) {
        this.file = raf;
        this.fc = fc;
        lock = new ReentrantLock();
    }

    LoggerImpl(RandomAccessFile raf, FileChannel fc, int xChecksum) {
        this.file = raf;
        this.fc = fc;
        this.xChecksum = xChecksum;
        lock = new ReentrantLock();
    }

    // ----------------------------------(0v0)--> 分割线--------------------------------------------------------------

    void init() {
        // 1. 获取文件大小、判断文件是否合法
        long size = 0;
        try {
            size = file.length();
        } catch (IOException e) {
            Panic.panic(e);
        }
        if (size < 4) {
            Panic.panic(Error.BadLogFileException);
        }
        // 2. 获取 log 文件的 大小和 xchecksum
        ByteBuffer raw = ByteBuffer.allocate(4);
        try {
            fc.position(0);
            fc.read(raw);
        } catch (IOException e) {
            Panic.panic(e);
        }
        int xChecksum = Parser.parseInt(raw.array());
        this.fileSize = size;
        this.xChecksum = xChecksum;
        // 3. 获取正常 log 文件，移除可能存在的 badtail
        checkAndRemoveTail();

    }

    // 检查并移除 bad tail
    private void checkAndRemoveTail() {
        rewind();

        int xCheck = 0;
        while (true) {
            byte[] log = internNext();
            if (log == null) break;
            xCheck = calChecksum(xCheck, log);
        }
        if (xCheck != xChecksum) {
            Panic.panic(Error.BadLogFileException);
        }

        try {
            truncate(position);
        } catch (Exception e) {
            Panic.panic(e);
        }
        try {
            file.seek(position);
        } catch (IOException e) {
            Panic.panic(e);
        }
        rewind();
    }


    // 计算单条日志的校验和
    private int calChecksum(int xCheck, byte[] log) {
        for (byte b :log) {
            xCheck = xCheck * SEED + b;
        }
        return xCheck;
    }

    @Override
    public void log(byte[] data) {
        byte[] log = wrapLog(data);
        ByteBuffer buf = ByteBuffer.wrap(log);
        lock.lock();
        try {
            fc.position(fc.size());
            fc.write(buf);
        } catch (IOException e) {
            Panic.panic(e);
        } finally {
            lock.unlock();
        }
        updateXChecksum(log);
    }

    // 更新日志文件的检验和
    private void updateXChecksum(byte[] log) {
        this.xChecksum = calChecksum(this.xChecksum,log);
        try {
            fc.position(0);
            fc.write(ByteBuffer.wrap(Parser.int2Byte(xChecksum)));
            fc.force(false);
        } catch (IOException e) {
            Panic.panic(e);
        }
    }

    // 将一条日志数据拼接成完整的日志格式
    private byte[] wrapLog(byte[] data) {
        byte[] checksum = Parser.int2Byte(calChecksum(0, data));
        byte[] size = Parser.int2Byte(data.length);
        return Bytes.concat(size, checksum, data);
    }


    // 指定位置截断
    @Override
    public void truncate(long x) throws Exception {
        lock.lock();
        try {
            fc.truncate(x);
        } finally {
            lock.unlock();
        }
    }

    // 返回日志数据
    private byte[] internNext() {
        // 判断文件大小是否合法
        if (position + OF_DATA >= fileSize) {
            return null;
        }
        // 读取日志 size
        ByteBuffer tmp = ByteBuffer.allocate(4);
        try {
            fc.position(position);
            fc.read(tmp);
        } catch (IOException e) {
            Panic.panic(e);
        }
        int size = Parser.parseInt(tmp.array());
        // 判断 position 是否已经读到了文件末尾
        if (position + size + OF_DATA > fileSize) {
            return null;
        }

        // 读取 checksum + data
        ByteBuffer buf = ByteBuffer.allocate(OF_DATA + size);
        try {
            fc.position(position);
            fc.read(buf);
        } catch (IOException e) {
            Panic.panic(e);
        }
        // 获得 log 数据
        byte[] log = buf.array();

        // 校验 checksum
        int checkSum1 = calChecksum(0, Arrays.copyOfRange(log, OF_DATA, log.length));
        int checkSum2 = Parser.parseInt(Arrays.copyOfRange(log, OF_CHECKSUM, OF_DATA));
        if (checkSum1 != checkSum2) {
            return null;
        }
        // 文件读取位置偏移到下一条日志开头
        position += log.length;
        return log;
    }

    // 迭代器方式
    //不断地从文件中读取下一条日志，并将其中的 Data 解析出来并返回
    @Override
    public byte[] next() {
        lock.lock();
        try {
            byte[] log = internNext();
            if (log == null) return null;
            return Arrays.copyOfRange(log, OF_DATA, log.length);
        } finally {
            lock.unlock();
        }
    }

    // ----------------------------------(0v0)--> 分割线--------------------------------------------------------------

    // 重置 position 文件读取位置
    @Override
    public void rewind() {
        position = 4;
    }

    @Override
    public void close() {
        try {
            fc.close();
            file.close();
        } catch (Exception e) {
            Panic.panic(e);
        }
    }
}
